/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2007-2008 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 *
 * --------------------------------------------------------------------------
 * $Id: ConnectionManager.java 2977 2008-04-27 15:41:42Z benoitf $
 * --------------------------------------------------------------------------
 */
package br.unifor.util.cluster.jgcs;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.SocketAddress;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import net.sf.jgcs.ControlSession;
import net.sf.jgcs.JGCSException;
import net.sf.jgcs.NotJoinedException;
import net.sf.jgcs.membership.MembershipListener;
import net.sf.jgcs.membership.MembershipSession;

import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

// MEXIDO

/**
 * This class manages connections on a JGroups object (DistributedTree,
 * Channel). Calls on certain methods of those JGroups objects are routed here.
 * If the object is not connected, reconnections are performed. There is a
 * reconnection time out.
 * @author eyindanga
 */
public class ConnectionManager implements MembershipListener, InvocationHandler {

    /**
     * Logger.
     */
    private static Log logger = LogFactory.getLog(ConnectionManager.class);

    /**
     * Reconnection timeout.
     */
    private volatile long timeout;

    /**
     * Manage the connection of this object.
     */
    private final Object handled;

    /**
     * Handle methods define in this interface.
     */
    private final Class<?> itfHandled;

    /**
     * true if handled object is connected.
     */
    private volatile boolean connected = true;
    
    private ControlSession control;
    
    private Set<Method> alreadyHandledMethods = new HashSet<Method>();

    public ConnectionManager(final long timeout, final Object handled, final Class<?> itfHandled) {
        super();
        this.timeout = timeout;
        this.handled = handled;
        this.itfHandled = itfHandled;
    }

    public ConnectionManager(final long timeout, final Object handled) {
        super();
        this.timeout = timeout;
        this.handled = handled;
        itfHandled = null;
    }
  
    public Object getHandledObject() {
        return handled;
    }

    public long getTimeout() {
        return timeout;
    }

    public void setTimeout(final long timeout) {
        this.timeout = timeout;
    }

    public boolean isConnected() {
        return connected;
    }

    public synchronized Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
        Object ret = null;

        if (handleThisMethod(method)) {
            long startTime = System.currentTimeMillis();
            while (!connected && System.currentTimeMillis() - startTime < timeout) {
                wait(timeout);
            }
            if (connected) {
                // invoke
                return method.invoke(handled, args);
            } else {
                logger.error("Reconnection timeout of '" + timeout
                        + " ms' exceeded: unable to invoke " + method.getName()
                        + " with args " + Arrays.asList(args));
                throw new Exception("Reconnection timeout of '" + timeout
                        + " ms' exceeded: unable to invoke " + method.getName()
                        + " with args " + Arrays.asList(args));
            }

        }
        try {
            ret = method.invoke(handled, args);
        } catch (IllegalAccessException e) {
            logger.error("Cannot invoke the method {0}", method.getName(), e);
        } catch (IllegalArgumentException e) {
            logger.error("Cannot invoke the method {0}", method.getName(), e);
        } catch (InvocationTargetException e) {
            logger.error("Cannot invoke the method {0}", method.getName(), e);
        }
        return ret;
    }

    private boolean handleThisMethod(final Method method) {
        if (alreadyHandledMethods.contains(method)) {
            return true;
        }
        if (itfHandled == null) {
            // Handle all the methods excepted these defined in the class Object
            if (method.getDeclaringClass() == Object.class) {
                return false;
            } else {
                return true;
            }
        }
        Method[] handledMethods = itfHandled.getDeclaredMethods();
        boolean found = true;
        for (Method handledMethod : handledMethods) {
            if (method.equals(handledMethod)) {
                alreadyHandledMethods.add(method);
                return true;
            }
            if (method.getName().equals(handledMethod.getName())) {
                Class<?>[] paramTypes = method.getParameterTypes();
                Class<?>[] expectedParamTypes = handledMethod.getParameterTypes();
                if (paramTypes.length == expectedParamTypes.length) {
                    for (int i = 0; i < paramTypes.length; i++) {
                        if (!paramTypes[i].equals(expectedParamTypes[i])) {
                            found = false;
                            break;
                        }
                    }
                    if (found) {
                        alreadyHandledMethods.add(method);
                        return true;
                    }
                }
            }
        }
        return false;
    }

	public void setControlSession(ControlSession control) {
		this.control = control;
	}
	
	////////////////// jGCS membership callbacks //////////////////
    
	public void onExcluded() {
		// FIXME aprimorar esse código de modo a tentar reconectar mais de uma vez
		logger.error("I was excluded from the group. Trying to re-join...");
		connected = false;
		try {
			control.join();
		} catch (JGCSException e) {
			logger.error("Error re-joining to the group", e);
		}
	}

	public void onMembershipChange() {
		try {
			MembershipSession m = ((MembershipSession) control);
			SocketAddress localAddr = m.getLocalAddress();
			
			if (!connected) {
				if (m.getMembership().getJoinedMembers().contains(localAddr)) {
					connected = true;
					synchronized (this) {
						notifyAll();
					}					
				}
			}
		} catch (NotJoinedException e) {
			logger.error("Error on membership change", e);
		}
	}

}
